//  Copyright (c) Microsoft Corporation.  All Rights Reserved.

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using System.Security.Principal;

using System.IdentityModel.Claims;
using System.IdentityModel.Policy;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;


namespace Saml20
{
    public class Saml20SecurityTokenAuthenticator : SecurityTokenAuthenticator
    {
        public static TimeSpan DefaultMaxSkew = new TimeSpan(0, 5, 0);

        TimeSpan clockSkew;
        List<SecurityTokenAuthenticator> supportingAuthenticators;

        public Saml20SecurityTokenAuthenticator(IEnumerable<SecurityTokenAuthenticator> supportingAuthenticators)
            : this(supportingAuthenticators, DefaultMaxSkew)
        {
        }

        public Saml20SecurityTokenAuthenticator(IEnumerable<SecurityTokenAuthenticator> supportingAuthenticators, TimeSpan clockSkew)
        {
            this.clockSkew = clockSkew;
            foreach (SecurityTokenAuthenticator tokenAuthenticator in supportingAuthenticators)
            {
                if (this.supportingAuthenticators == null)
                    this.supportingAuthenticators = new List<SecurityTokenAuthenticator>();

                this.supportingAuthenticators.Add(tokenAuthenticator);
            }
        }

        protected override bool CanValidateTokenCore(SecurityToken token)
        {
            return (token is Saml20SecurityToken);
        }

        protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token)
        {
            if (token == null)
                throw new ArgumentNullException("token");

            Saml20SecurityToken samlToken = token as Saml20SecurityToken;

            if (samlToken == null)
                throw new ArgumentException("token");

            if (samlToken.Assertion.SigningToken == null)
                throw new SecurityTokenException("The SAML token is not signed.");

            if (samlToken.Assertion.Conditions != null)
            {
                DateTime effectiveTime = samlToken.Assertion.Conditions.NotBefore;
                DateTime expirationTime = samlToken.Assertion.Conditions.NotOnOrAfter;
                DateTime curEffectiveTime = (effectiveTime < DateTime.MinValue.Add(this.clockSkew)) ? effectiveTime : effectiveTime.Subtract(this.clockSkew);
                DateTime curExpirationTime = (expirationTime > DateTime.MaxValue.Subtract(this.clockSkew)) ? expirationTime : expirationTime.Add(this.clockSkew);
                DateTime curTime = DateTime.UtcNow;

                if (!(curEffectiveTime.ToUniversalTime() <= curTime) && (curTime < curExpirationTime.ToUniversalTime()))
                    throw new SecurityTokenException("The SAML token is not time effective.");
            }

            ClaimSet issuerClaimset = ResolveClaimSet(samlToken.Assertion, samlToken.Assertion.SigningToken);

            List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
            foreach (Saml20Statement statement in samlToken.Assertion.Statements)
            {
                policies.Add(statement.CreatePolicy(issuerClaimset, samlToken.Assertion, this));
            }

            return policies.AsReadOnly();
        }

        public virtual ClaimSet ResolveClaimSet(Saml20Assertion assertion, SecurityToken token)
        {
            if (token == null)
                throw new ArgumentNullException("token");

            for (int i = 0; i < this.supportingAuthenticators.Count; ++i)
            {
                if (this.supportingAuthenticators[i].CanValidateToken(token))
                {
                    ReadOnlyCollection<IAuthorizationPolicy> authorizationPolicies = this.supportingAuthenticators[i].ValidateToken(token);
                    AuthorizationContext authContext = AuthorizationContext.CreateDefaultAuthorizationContext(authorizationPolicies);
                    if (authContext.ClaimSets.Count > 0)
                    {
                        return authContext.ClaimSets[0];
                    }
                }
            }
            return null;
        }

        public virtual ClaimSet ResolveClaimSet(Saml20Assertion assertion, SecurityKeyIdentifier keyIdentifier)
        {
            if (keyIdentifier == null)
                throw new ArgumentNullException("keyIdentifier");

            RsaKeyIdentifierClause rsaKeyIdentifierClause;
            EncryptedKeyIdentifierClause encryptedKeyIdentifierClause;
            if (keyIdentifier.TryFind<RsaKeyIdentifierClause>(out rsaKeyIdentifierClause))
            {
                return new DefaultClaimSet(new Claim(ClaimTypes.Rsa, rsaKeyIdentifierClause.Rsa, Rights.PossessProperty));
            }
            else if (keyIdentifier.TryFind<EncryptedKeyIdentifierClause>(out encryptedKeyIdentifierClause))
            {
                return new DefaultClaimSet(Claim.CreateHashClaim(encryptedKeyIdentifierClause.GetBuffer()));
            }

            return null;
        }

        public virtual IIdentity ResolveIdentity(Saml20Assertion assertion, SecurityToken token)
        {
            if (token == null)
                throw new ArgumentNullException("token");

            for (int i = 0; i < this.supportingAuthenticators.Count; ++i)
            {
                if (this.supportingAuthenticators[i].CanValidateToken(token))
                {
                    ReadOnlyCollection<IAuthorizationPolicy> authorizationPolicies = this.supportingAuthenticators[i].ValidateToken(token);
                    AuthorizationContext context = AuthorizationContext.CreateDefaultAuthorizationContext(authorizationPolicies);
                    if (context.Properties.ContainsKey("Identities"))
                        return (IIdentity)(context.Properties["Identities"]);
                }
            }

            return null;
        }

        public virtual IIdentity ResolveIdentity(Saml20Assertion assertion, SecurityKeyIdentifier keyIdentifier)
        {
            if (keyIdentifier == null)
                throw new ArgumentNullException("keyIdentifier");

            RsaKeyIdentifierClause rsaKeyIdentifierClause;
            if (keyIdentifier.TryFind<RsaKeyIdentifierClause>(out rsaKeyIdentifierClause))
            {
                return new GenericIdentity(rsaKeyIdentifierClause.Rsa.ToXmlString(false), this.GetType().Name);
            }

            return null;
        }
    }
}
